﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using WeatherAggregator.WeatherSources;

namespace WeatherAggregator
{
	/// <summary>
	/// Собирает данные с помощью источников данных.
	/// Записывает данные в базу.
	/// </summary>
	public class Aggregator
	{
		readonly TimeSpan _defaultInterval = new TimeSpan(0, 2, 0);
		readonly List<IWeatherDataSource> _sources = new List<IWeatherDataSource>();

		readonly SortedDictionary<DateTime, List<IWeatherDataSource>> _timeline =
			new SortedDictionary<DateTime, List<IWeatherDataSource>>();

		readonly Object _timerSync = new Object();

		Timer _t;

		public Aggregator(string connectionString)
		{
			ConnectionString = connectionString;
		}

		public string ConnectionString { get; private set; }

		public void Start()
		{
			lock (_timerSync)
			{
				// Выставим в источники даты, события до которых нас уже не интересуют.
				using (DataContext c = GetDataContext())
				{
					foreach (IWeatherDataSource src in _sources)
					{
						IWeatherDataSource srcClosure = src;
						WeatherDataEntry lastEntry = c.WeatherDataEntries
							.Where(_ => _.SourceId == srcClosure.SourceId && _.CityName == srcClosure.PointName)
							.OrderByDescending(_ => _.TimeUtc)
							.FirstOrDefault(); // дата последнего события от источника
						if (lastEntry == null)
							continue;

						src.LastGatheredData = lastEntry.TimeUtc;
					}
				}


				_timeline.Add(DateTime.UtcNow, new List<IWeatherDataSource>(_sources));

				_t = new Timer(TimerCallback, null, 10, Timeout.Infinite);
			}
		}

		public void Stop()
		{
			lock (_timerSync)
			{
				_t.Dispose();
				_t = null;
				_timeline.Clear();
				// TODO
			}
		}

		public void AddSource(IWeatherDataSource src)
		{
			if (src == null) throw new ArgumentNullException("src");

			if (_timeline.Count > 0)
				throw new NotImplementedException("добавление источников после старта не сделал");

			_sources.Add(src);
		}

		public void AddAlarm(DateTime time, IWeatherDataSource source)
		{
			List<IWeatherDataSource> timelinePoint;
			if (!_timeline.TryGetValue(time, out timelinePoint))
			{
				timelinePoint = new List<IWeatherDataSource>();
				_timeline.Add(time, timelinePoint);
			}
			timelinePoint.Add(source);
		}

		void TimerCallback(object state)
		{
			lock (_timerSync)
			{
				TimeSpan nextAlarmDelay = _defaultInterval;

				try
				{
					DateTime opStartTime = DateTime.UtcNow;

					// определим, какие источники были "потеряны" в timeline. Наприер, из за исключения
					foreach (IWeatherDataSource source in _sources)
					{
						if (source == null) continue;

						Boolean found = false;
						foreach (var tlEntry in _timeline.Values)
						{
							found = tlEntry.Any(_ => ReferenceEquals(source, _));
							if (found) break;
						}
						if (!found)
							AddAlarm(opStartTime + _defaultInterval, source);
					}

					// определим, какие события нам сейчас надо обработать и убрать из плана запуска
					List<KeyValuePair<DateTime, List<IWeatherDataSource>>> toDel = (from entryArr in _timeline
					                                                                where entryArr.Key <= opStartTime
					                                                                select entryArr).ToList();
					// а удалять будем. в самом конце. вдруг, ексепшен?


					List<IWeatherDataSource> toCheck = (from entry in toDel
					                                    from src in entry.Value
					                                    where src != null
					                                    select src).ToList();

					// ну и обработаем их
					var newData = new List<WeatherDataEntry>();

					foreach (IWeatherDataSource src in toCheck)
					{
						if (src == null) continue;

						WeatherDataSourceRespond respond = src.GetNewData();

						DateTime addTime = opStartTime + Helper.Max(respond.RecommendedDelay, TimeSpan.Zero);

						AddAlarm(addTime, src);

						if (respond.Data != null)
							newData.AddRange(respond.Data.Where(_ => _ != null));
					}

					CommitData(newData);

					DateTime opEndTime = DateTime.UtcNow;

					// пока собирали данные, могли проворонить какие то запланированные действия. особенно в дебаге.
					int skippedAlarmsCount = (from a in _timeline
					                          where a.Key > opStartTime && a.Key < opEndTime
					                          select a).Count();


					KeyValuePair<DateTime, List<IWeatherDataSource>> nextAlarm = _timeline.FirstOrDefault(_ => _.Key > opStartTime);
						// тут отработанные записи ещё не удалены

					if (skippedAlarmsCount > 0)
					{
						nextAlarmDelay = TimeSpan.Zero; // если чего то пропустили - сразуповторм сбор.
					}
					else if (nextAlarm.Key != default(DateTime))
					{
						nextAlarmDelay = nextAlarm.Key - opEndTime;
						if (nextAlarmDelay < TimeSpan.Zero) nextAlarmDelay = TimeSpan.Zero;
					}


					if (Aggregation != null)
						Aggregation(this, new AggregationInfoEventArgs
						                  	{
						                  		NextExpectedFire = nextAlarm.Key,
						                  		GatheredEntriesCount = newData.Count,
						                  		CheckedSourcesCount = toCheck.Count,
						                  	}
							);

					// удалим обработанные записи.
					toDel.ForEach(_ => _timeline.Remove(_.Key));
				}
				catch (Exception ex)
				{
					if (Error != null)
						Error(this, new ThreadExceptionEventArgs(ex));

					// если после ошибки никаких планов не осталось - запланируем пустое событие - оно соберёт все пропущенные даные
					DateTime now = DateTime.UtcNow;
					bool hasFuture = _timeline.Where(_ => _.Key > now).Count() > 0;
					if (!hasFuture)
						AddAlarm(now + _defaultInterval, null);
				}
				finally
				{
					_t.Change((Int32) nextAlarmDelay.TotalMilliseconds + 100, Timeout.Infinite);
				}
			}
		}

		public event EventHandler<AggregationInfoEventArgs> Aggregation;
		public event EventHandler<ThreadExceptionEventArgs> Error;

		void CommitData(ICollection<WeatherDataEntry> newData)
		{
			if (newData == null || newData.Count == 0) return;
			DataContext c = GetDataContext();

			c.WeatherDataEntries.InsertAllOnSubmit(newData);
			c.SubmitChanges();
		}


		DataContext GetDataContext()
		{
			DataContext c = Helper.GetDataContext(ConnectionString);

			return c;
		}
	}
}